package application.models.models;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import application.models.Observer;
import application.models.collections.CarCollection;
import application.models.datalayer.Attribute;
import application.models.datalayer.DataCondition;
import application.models.datalayer.Date;
import application.models.datalayer.Operator;
import application.models.exceptions.CarMatchException;
import application.models.exceptions.InvalidAttributeException;
import application.models.exceptions.MissingDataException;

/**
 * A subclass of AbstractModel. Describes reservations.
 * 
 * @author Morten Therkildsen & Philip Rasmussen
 */
public class ReservationModel extends AbstractModel {
	private static SetDescription setDescription = new SetDescription("id", "reservations", "cardescriptions", "cardescription_id", "id");
	private static Set<Observer> globalObservers = new HashSet<Observer>(); // A set containing all global observers for any model of this kind
	private static boolean isIterating = false;

	/**
	 * The id attribute of the main set
	 * @return The id attribute of the main set
	 */
	public static String getMainSetIdAttribute(){

		return setDescription.getMainSetIdAttribute();
	}

	/**
	 * The name of the main set
	 * @return The name of the main set
	 */
	public static String getMainSetName(){

		return setDescription.getMainSetName();
	}

	/**
	 * The join attribute of the main set
	 * @return The join attribute of the main set
	 */
	public static String getMainSetJoinAttribute(){

		return setDescription.getMainSetJoinAttribute();
	}

	/**
	 * The name of the joined set
	 * @return The name of the joined set
	 */
	public static String getJoinedSetName(){

		return setDescription.getJoinedSetName();
	}

	/**
	 * The join attribute of the joined set
	 * @return The join attribute of the joined set
	 */
	public static String getJoinedSetJoinAttribute(){

		return setDescription.getJoinedSetJoinAttribute();
	}	

	/**
	 * Notifies all globally registered observers about a model update
	 * 
	 * While running global observers cannot be added, removed or notified
	 * 
	 * @param object	The model that observers should be notified about
	 */
	protected static void notifyGlobalObservers(ReservationModel object){

		if(isIterating) return;
		
		isIterating = true;
		for(Observer observer : globalObservers){
			observer.notify(object);
		}
		isIterating = false;
	}

	/**
	 * Registers an observer to receive notifications about any object of this kind
	 * An observer may only be added once - subsequent adds of the same observer are ignored
	 * 
	 * @param observer		The observer to add
	 * @return	true if the observer successfully was added, otherwise false
	 */
	public static boolean addGlobalObserver(Observer observer){

		if(observer == null) throw new NullPointerException("observer was null!");
		
		if(!isIterating){
			globalObservers.add(observer);
			return true;
		}
		
		return false;
	}

	/**
	 * Removes a previously set global observer
	 * Nothing will happen if observer has not been added beforehand
	 * 
	 * @param observer		A previously added observer
	 * @return	true if the observer successfully was removed, otherwise false
	 */
	public static boolean removeGlobalObserver(Observer observer){

		if(!isIterating){
			globalObservers.remove(observer);
			return true;
		}
		
		return false;
	}

	/**
	 * Loads an entity of this model type that matches the id value 
	 * @param The value of the id attribute for the entity
	 * @return The entity matching the value of the id attribute
	 */
	public static ReservationModel load(int id) {

		try{

			/* Unchecked type casting is okay, since application.models.models.ReservationModel is in fact of type ReservationModel */
			return (ReservationModel) loadHelper(id, (Class<ReservationModel>) Class.forName("application.models.models.ReservationModel"), setDescription);
		}
		catch(ClassNotFoundException e){throw new RuntimeException("Unexpected logic error. Update package name for the model.", e);}		
	}

	/**
	 * Creates an entity of this model type
	 * @param mainSetIdAttributeValue the value of the id attribute
	 * @param mainSetJoinAttributeValue the value of the sets joinAttribute
	 */
	public ReservationModel(Object mainSetIdAttributeValue, Object mainSetJoinAttributeValue){

		super(mainSetIdAttributeValue, mainSetJoinAttributeValue, setDescription);
	}

	/**
	 * Creates an entity of this model type
	 */
	public ReservationModel(){

		this(null, null);
	}

	/**
	 * Returns the car model that matches the reservation the best based on the current state of the system.
	 * A car cannot be found if the reservation has no start and/or end dates
	 * 
	 * @return	the CarModel representing the reserved car or null if none were found
	 */
	public CarModel getCurrentCar(){

		try{
			getId(); // If id is not set, i.e. the model is not saved we not proceed
			
			if(!isDeleted()) {
				
				Set<ReservationModel> set = new HashSet<ReservationModel>();
				set.add(this);
				
				try{
					
					Map<ReservationModel, CarModel> map = ReservationToCarMatcher.matchCarsToReservations(set);
					
					return map.get(this);
					
				} catch(CarMatchException e){return null;}
			}
		} catch(NullPointerException e){/* IGNORE */}
		
		// This is run if getId() threw an exception or the model is deleted
		
		try{
			
			Set<CarModel> set = getMatchingFreeCars();
			
			if(set.size() == 0) return null;
			else return set.iterator().next();
			
		} catch(MissingDataException e){return null;}
	}
	
	/**
	 * Returns the criteria for a car the reservation keeps
	 * 
	 * @return	an array of DataConditions matching the criteria
	 */
	public DataCondition[] getReservationCriteria(){
		
		ArrayList<DataCondition> conditions = new ArrayList<DataCondition>();
		
		for(Attribute attribute : this){
			
			if(attribute.getDataSetOwner().equals(setDescription.getJoinedSetName()) && attribute.getValue() != null){
				
				conditions.add(new DataCondition(attribute.getName(), Operator.EQUALS, attribute.getValue()));
			}
		}
		
		return conditions.toArray(new DataCondition[0]);
	}
	
	/**
	 * Returns a collection containing the cars currently matching the reservation criteria.
	 * Does not check whether the card are available in the reservation period.
	 * 
	 * @return a collection of cars matching the reservation criteria
	 */
	public CarCollection getMatchingCars(){
		
		CarCollection collection = new CarCollection();
		
		for(DataCondition condition : getReservationCriteria()){
			
			try {collection.addFilter(condition);}
			catch(InvalidAttributeException e){/* Ignore */}
		}
		
		try{collection.load();}
		catch(IOException e){throw new RuntimeException("Unexpected logic failure.");}
		
		return collection;
	}
	
	/**
	 * Returns a collection containing the cars currently matching the reservation criteria.
	 * The cars returned are all free in the reservation period.
	 * 
	 * If the period is not set for the reservation, null will be returned
	 * 
	 * @return a collection of free cars matching the reservation criteria in the reservation period
	 * @throws MissingDataException if the "start" and/or "ends" attribute has not been set
	 */
	public Set<CarModel> getMatchingFreeCars() throws MissingDataException {
		
		try{ // DOES NOT HANDLE: attributes are changed but not saved
			
			if(getAttribute("starts") == null || getAttribute("ends") == null) throw new MissingDataException("\"starts\" and/or \"ends\" was not set");

			Set<CarModel> set = new HashSet<CarModel>();

			for(CarModel model : getMatchingCars()){

				ReservationModel[] matches = model.getReservationsInPeriod((Date) getAttribute("starts"), (Date) getAttribute("ends"));
				if(matches.length == 0 || (matches.length == 1 && matches[0] == this)) set.add(model);
			}

			return set;
		
		} catch(InvalidAttributeException e){throw new RuntimeException("Unexpected logic failure.", e);}
	}
	
	/**
	 * Returns the customer which this reservation belongs to
	 * 
	 * @return	A CustomerModel representing the owner of the reservation
	 */
	public CustomerModel getCustomer(){
		
		try{
			if(getAttribute("customer_no") == null) return null;
			
			return CustomerModel.load(((Integer) getAttribute("customer_no")).intValue());
			
		} catch(InvalidAttributeException e){ throw new RuntimeException("Unexpected logic failure.", e);}
	}

	/**
	 * Notifies all observers when called
	 */
	protected void notifyObservers(){

		super.notifyObservers();
		notifyGlobalObservers(this);
	}

	/**
	 * Validates that an attribute have met the data layer criteria
	 *  
	 * @param An attribute object
	 * @return	True if validation succeeds, false if attribute has a null value
	 */
	protected boolean mandatoryAttributeValidation(Attribute attribute){

		/* If a mandatory attribute is null it will cause a runtime exception. */
		if(attribute.isMandatory() && attribute.getValue() == null) return false;

		return true;
	}

	/**
	 * Validates that the object's value is valid. (ust not be empty)
	 */
	protected boolean attributeValueValidation(Object value){

		if(value instanceof String)
			if(value.toString().trim().length() == 0) return false;
		
		return true;
	}
}
